package top.someapp.applib.ext;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.UnknownFormatConversionException;
import top.someapp.applib.AppException;

/**
 * @author zwz
 * Created on 2020-05-23
 */
public class DateTime extends Date {

    private Calendar calendar;
    private static final SimpleDateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
        Locale.US);

    /// -------------------------------------------------------------------------------------------- constructors
    public DateTime() {
        this(System.currentTimeMillis());
    }

    public DateTime(long date) {
        super(date);
        calendar = Calendar.getInstance();
        calendar.setTime(this);
    }

    public DateTime(Calendar calendar) {
        super(calendar.getTimeInMillis());
        this.calendar = calendar;
    }
    /// -------------------------------------------------------------------------------------------- ~constructors

    /// -------------------------------------------------------------------------------------------- INSTANCE helpers
    public static DateTime now() {
        return new DateTime();
    }

    public static DateTime of(int year, int month, int day) {
        return of(year, month, day, 0, 0, 0, 0);
    }

    public static DateTime of(
        int hour, int minute, int second, int ms) {
        return of(0, 0, 0, hour, minute, second, ms);
    }

    public static DateTime of(java.sql.Date sqlDate) {
        return new DateTime(sqlDate.getTime());
    }

    public static DateTime of(
        int year, int month, int day, int hour, int minute, int second, int ms) {
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.YEAR, year);
        calendar.set(Calendar.MONTH, month - 1 > 12 ? month % 12 : month - 1);
        calendar.set(Calendar.DATE, day > 31 ? (day % 31) : day);
        calendar.set(Calendar.HOUR_OF_DAY, hour % 24);
        calendar.set(Calendar.MINUTE, minute % 60);
        calendar.set(Calendar.SECOND, second % 60);
        calendar.set(Calendar.MILLISECOND, ms % 1000);
        return new DateTime(calendar);
    }

    public static DateTime fromIso8601(String dtIso8601) {
        if (dtIso8601 == null || dtIso8601.length() < 19) {
            throw new IllegalArgumentException(dtIso8601 + " is not a iso8601 format datetime!!");
        }
        try {
            return new DateTime(iso8601.parse(dtIso8601).getTime());
        } catch (Exception ignored) {
        }
        return null;
    }
    /// -------------------------------------------------------------------------------------------- ~INSTANCE helpers

    public int year() {
        return calendar.get(Calendar.YEAR);
    }

    public int month() {
        return calendar.get(Calendar.MONTH) + 1;
    }

    public int day() {
        return calendar.get(Calendar.DATE);
    }

    public int hour() {
        return calendar.get((Calendar.HOUR_OF_DAY));
    }

    public int minute() {
        return calendar.get(Calendar.MINUTE);
    }

    public int second() {
        return calendar.get(Calendar.SECOND);
    }

    public int milliSecond() {
        return calendar.get(Calendar.MILLISECOND);
    }

    public int dateOnly() {
        return year() * 10000 + month() * 100 + day();
    }

    public int timeOnly() {
        return hour() * 10000 + minute() * 100 + second();
    }

    public String dateTimeDigits() {
        return dateOnly() + "-" + timeOnly();
    }

    public String iso8601() {
        return String
            .format("%04d-%02d-%02dT%02d:%02d:%02d", year(), month(), day(), hour(), minute(),
                second());
    }

    public String iso8601WithMS() {
        return String
            .format("%04d-%02d-%02dT%02d:%02d:%02d.%03d", year(), month(), day(), hour(), minute(),
                second(), milliSecond());
    }

    public String formatDateOnly(String yearName, String monthName, String dayName) {
        return String
            .format("%04d%s%02d%s%02d%s", year(), yearName, month(), monthName, day(), dayName);
    }

    public String formatTimeOnly(String hourName, String minuteName, String secondName) {
        return String.format("%02d%s%02d%s%02d%s", hour(), hourName, minute(), minuteName, second(),
            secondName);
    }

    public String formatTimeAmPmPrefix(String am, String pm) {
        String prefix = am;
        int h = hour();
        if (hour() > 12) {
            prefix = pm;
            h -= 12;
        }
        return String.format("%s%d:%02d:%02d", prefix, h, minute(), second());
    }

    public String cnWeekDay() {
        switch (calendar.get(Calendar.DAY_OF_WEEK)) {
            case Calendar.MONDAY:
                return "星期一";
            case Calendar.TUESDAY:
                return "星期二";
            case Calendar.WEDNESDAY:
                return "星期三";
            case Calendar.THURSDAY:
                return "星期四";
            case Calendar.FRIDAY:
                return "星期五";
            case Calendar.SATURDAY:
                return "星期六";
            case Calendar.SUNDAY:
                return "星期日";
        }
        throw new UnknownFormatConversionException("未知的日期");
    }

    public java.sql.Date toSQLDate() {
        return new java.sql.Date(getTime());
    }

    /// -------------------------------------------------------------------------------------------- computes
    public DateTime addDays(int days) {
        return addSeconds(days * 86400);
    }

    public DateTime addSeconds(int seconds) {
        if (seconds == 0) {
            return this;
        }
        calendar.add(Calendar.SECOND, seconds);
        return updateTime();
    }

    public DateTime addMillSeconds(int millSeconds) {
        if (millSeconds == 0) {
            return this;
        }
        calendar.add(Calendar.MILLISECOND, millSeconds);
        return updateTime();
    }

    public DateTime firstDayOfMonth() {
        if (day() == 1) {
            return this;
        }

        calendar.set(Calendar.DATE, 1);
        return updateTime();
    }

    public DateTime lastDayOfMonth() {
        final int m = month();
        switch (m) {
            case 1:
            case 3:
            case 5:
            case 7:
            case 8:
            case 10:
            case 12:
                calendar.set(Calendar.DATE, 31);
                return updateTime();
            case 4:
            case 6:
            case 9:
            case 11:
                calendar.set(Calendar.DATE, 30);
                return updateTime();
            case 2:
                calendar.set(Calendar.DATE, isLeapYear() ? 29 : 28);
                return updateTime();
            default:
                throw new AppException("Impossible month of date: " + m);
        }
    }

    public boolean isLeapYear() {
        int y = year();
        return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
    }

    /**
     * 往前/后翻日历qty个自然月忽略日和时分秒部分, 自然月不是按时间间隔计算的，如：2019-01-31往后翻一个月为201902，并不是+30天
     * 保证月份差 == qty
     *
     * @param qty 翻几个自然月, &gt; 0: 往后翻，&lt; 0: 往前翻
     * @return 形如yyyyMM的日期
     */
    public int flipNatureMonth(int qty) {
        int y = year();
        int m = month();
        if (qty == 0) {
            return y * 100 + m;
        }

//        int dy = Math.abs(qty) / 12;
//        int dm = Math.abs(qty) % 12;
//        if (qty > 0) {
//            if (m + dm > 12) { // 进位
//                m = (m + dm) % 12;
//                dy++;
//            } else {
//                m += dm;
//            }
//        } else {
//            if (m - dm < 1) { // 借位
//                dy--;
//                m = (m + 12) - dm;
//            } else {
//                m -= dm;
//            }
//        }
//        return (y + dy) * 100 + m;
        int ms = y * 12 + m + qty;    // 全部换算成月来计算
        return ms % 12 == 0 ? (ms / 12 - 1) * 100 + 12 : (ms / 12) * 100 + (ms % 12);
    }
    /// -------------------------------------------------------------------------------------------- ~computes

    private DateTime updateTime() {
        setTime(calendar.getTimeInMillis());
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        if (!super.equals(o)) {
            return false;
        }
        DateTime dateTime = (DateTime) o;
        return Objects.equals(calendar, dateTime.calendar);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), calendar);
    }

    @Override
    public String toString() {
        return iso8601WithMS();
    }
}

